
#include "stdafx.h"


syslog_params_t syslog_params;


struct syslog_file_t
{
	syslog_file_t( const syslog_params_t* p ) : params( p ) { }
	~syslog_file_t( ) { close( ); }

	syslog_file_t( syslog_file_t&& temp ) { takeup( temp ); }
	syslog_file_t& operator=( syslog_file_t&& temp ) { return takeup( temp ); }

	syslog_file_t& takeup( syslog_file_t& old )
	{
		close( );

		_tcscpy_s( logfullname, old.logfullname );

		logfile = old.logfile;
		old.logfile = null_file_handle;

		params = old.params;

		return *this;
	}

	bool compose_name( date_time dt )
	{
		if( params->have_console && params->using_stdout )
		{
			logfullname[ 0 ] = _T( '\0' );

			return true;
		}

		const size_t max_date_buf = 20; // min 17
		TCHAR date_buf[ max_date_buf ];

		switch( params->logrotate_condition )
		{
		case to_year:
			_stprintf_s( date_buf, _T( "%04d" ), dt.year );
			break;
		case to_month:
			_stprintf_s( date_buf, _T( "%04d-%02d" ), dt.year, dt.month );
			break;
		case to_day:
			_stprintf_s( date_buf, _T( "%04d-%02d-%02d" ), dt.year, dt.month, dt.day );
			break;
		case to_hour:
			_stprintf_s( date_buf, _T( "%04d-%02d-%02d=%02d" ), dt.year, dt.month, dt.day, dt.hours );
			break;
		case to_minute:
			_stprintf_s( date_buf, _T( "%04d-%02d-%02d=%02d-%02d" ), dt.year, dt.month, dt.day, dt.hours, dt.minutes );
			break;
		}

		return ( -1 ) != _stprintf_s( logfullname, _T( "%s\\%s%s%s" ), params->logpath, params->logname, date_buf, params->logextension );
	}

	file_handle_t open( )
	{
		if( !logfile )
			if( params->using_stdout )
				logfile = open_stdout( );
			else
			{
				unset_read_only_attrib( logfullname );

				logfile = open_logfile( logfullname );
			}

		return logfile;
	}

	void close( )
	{
		if( logfile )
		{
			if( !params->using_stdout && params->mark_as_readonly )
				set_read_only_attrib( logfullname );

			::close( logfile );
		}
	}

	bool log_message( date_time timestamp, IN_ADDR sender_addr, const byte message[ ], size_t msg_len, bool own_msg = false );

	template< size_t MSG_LEN > void log_own_msg( date_time timestamp, const byte( &msg )[ MSG_LEN ] )
	{
		/* return */ log_message( timestamp, syslog_params_t::address_loopback, msg, MSG_LEN - 1 /* without trailing '\0' */, true );
	}


	TCHAR logfullname[ SYSLOG_MAX_PATH ];
	file_handle_t logfile;
	const syslog_params_t* params;
};


bool syslog_file_t::log_message( date_time timestamp, IN_ADDR sender_addr, const byte message[ ], size_t msg_len, bool own_msg )
{
	if(
		! logfile ||
		message == nullptr ||
		msg_len == 0
	)
		return false; // invalid arguments

	int PRI = -1;
	size_t displacement = 0;

	if( msg_len >= 3 && message[ 0 ] == '<' && isdigit( message[ 1 ] ) )
	{
		if( message[ 2 ] == '>' )
			displacement = 3;
		else if( msg_len >= 4 && isdigit( message[ 2 ] ) )
		{
			if( message[ 3 ] == '>' )
				displacement = 4;
			else if( msg_len >= 5 && isdigit( message[ 3 ] ) )
			{
				if( message[ 4 ] == '>' )
					displacement = 5;
				else
					goto skip_filter;
			}
			else
				goto skip_filter;
		}
		else
			goto skip_filter;

		PRI = atoi( ( const char* ) message + 1 );

		if( 0 <= PRI && PRI <= 191 )
		{
			int severity = PRI & 0x7;

			if( own_msg )
			{
				if( severity >= params->own_filter )
					return false; // there has been no error, but we haven't logged the message
			}
			else
				if( severity >= params->filter )
					return false; // there has been no error, but we haven't logged the message
		}
	}

skip_filter:
	if( !own_msg )
		for( size_t i = 0; i < params->num_regex; ++i )
			if( params->regex_tests[ i ].regex.check( message + displacement ) )
				switch( params->regex_tests[ i ].mode )
			{
				case syslog_params_t::regex_mode::drop:
#if defined ( WINDOWSSYSLOG_DEBUG )
					_ftprintf( WSL_DBG_STREAM, WSL_DBG_PREFIX _T( "DROP MATCH: {%S} action: return\n" ), params->regex_tests[ i ].regex.expression );
#endif
					return false; // there has been no error, but we haven't logged the message
				case syslog_params_t::regex_mode::trim:
#if defined ( WINDOWSSYSLOG_DEBUG )
					_ftprintf( WSL_DBG_STREAM, WSL_DBG_PREFIX _T( "TRIM MATCH: {%S} action: continue\n" ), params->regex_tests[ i ].regex.expression );
#endif
					continue;
				case syslog_params_t::regex_mode::pass:
#if defined ( WINDOWSSYSLOG_DEBUG )
					_ftprintf( WSL_DBG_STREAM, WSL_DBG_PREFIX _T( "PASS MATCH: {%S} action: goto skip_regex\n" ), params->regex_tests[ i ].regex.expression );
#endif
					goto skip_regex;
			}
			else
				switch( params->regex_tests[ i ].mode )
			{
				case syslog_params_t::regex_mode::drop:
#if defined ( WINDOWSSYSLOG_DEBUG )
					_ftprintf( WSL_DBG_STREAM, WSL_DBG_PREFIX _T( "DROP NO MATCH: {%S} action: continue\n" ), params->regex_tests[ i ].regex.expression );
#endif
					continue;
				case syslog_params_t::regex_mode::trim:
#if defined ( WINDOWSSYSLOG_DEBUG )
					_ftprintf( WSL_DBG_STREAM, WSL_DBG_PREFIX _T( "TRIM NO MATCH: {%S} action: return\n" ), params->regex_tests[ i ].regex.expression );
#endif
					return false; // there has been no error, but we haven't logged the message
				case syslog_params_t::regex_mode::pass:
#if defined ( WINDOWSSYSLOG_DEBUG )
					_ftprintf( WSL_DBG_STREAM, WSL_DBG_PREFIX _T( "PASS NO MATCH: {%S} action: continue\n" ), params->regex_tests[ i ].regex.expression );
#endif
					continue;
			}

skip_regex:
#if defined ( WINDOWSSYSLOG_DEBUG )
	_ftprintf( WSL_DBG_STREAM, WSL_DBG_PREFIX _T( "LOG THE MESSAGE\n" ) );
#endif
	const size_t print_buffer_size = 128; // must be sufficient for printing all the fields: date, time (including milliseconds), ip etc AND separators (TAB characters)
	char print_buffer[ print_buffer_size ] = "";

	size_t chars_printed = 0;

	int ret = 0;

	if( params->log_date || params->log_time )
		if( ( ret = print_timestamp( print_buffer, print_buffer_size, timestamp, params->log_date, params->log_time, params->log_msec ) ) < 0 )
			return false;
		else
			chars_printed += ret;

	if( params->log_ip )
		if( ( ret = print_ip( print_buffer + chars_printed, print_buffer_size - chars_printed, sender_addr ) ) < 0 )
			return false;
		else
			chars_printed += ret;

	if( ( ret = print_facility_severity( print_buffer + chars_printed, print_buffer_size - chars_printed, PRI ) ) < 0 )
		return false;
	else
		chars_printed += ret;

	write( logfile, ( byte* ) print_buffer, chars_printed );

	if( ( msg_len - displacement ) > 0 )
	{
		write( logfile, message + displacement, msg_len - displacement );

		if( own_msg )
			if( params->newline_with_CR )
				write( logfile, ( byte* ) "\r\n", 2 );
			else
				write( logfile, ( byte* ) "\n", 1 );
	}

	if( params->flush )
		flush( logfile );

	return true;
}


struct syslog_status_t
{
	bool init( )
	{
		SvcStatus.dwCurrentState = SERVICE_STOPPED;
		SvcStatus.dwWin32ExitCode = NO_ERROR;
		SvcStatus.dwServiceSpecificExitCode = 0;

		SvcStatusHandle = nullptr;

		shutdown = false;
		shutdown_event = nullptr;

		return true;
	}

	SERVICE_STATUS			SvcStatus;
	SERVICE_STATUS_HANDLE	SvcStatusHandle;
	bool shutdown;
	WSAEVENT shutdown_event;
}
syslog_status;


enum SyslogErrorCodes
{
	SYSLOG_ERR_NOERROR,
	SYSLOG_ERR_UNSPECIFIED,
	SYSLOG_ERR_FILEOPEN,
	SYSLOG_ERR_WINSOCK_INIT,
	SYSLOG_ERR_WINSOCK_EVENT,
	SYSLOG_ERR_WINSOCK_SOCKET,
	SYSLOG_ERR_WINSOCK_BIND,
	SYSLOG_ERR_WINSOCK_SELECT,
	SYSLOG_ERR_SERVICE_STOP = 0x100,
	SYSLOG_ERR_SYSTEM_SHUTDOWN
};

const TCHAR* SyslogErrorMessages[ ] =
{
	_T( "No error" ),
	_T( "Unknown error" ),
	_T( "Error opening file" ),
	_T( "Error initializing WinSock" ),
	_T( "Error creating event" ),
	_T( "Error creating socket" ),
	_T( "Error binding socket" ),
	_T( "Error associating event with socket" )
};

VOID WINAPI SvcCtrlHandler( DWORD dwCtrl )
{
	switch( dwCtrl )
	{
	case SERVICE_CONTROL_STOP:
		syslog_status.SvcStatus.dwServiceSpecificExitCode = SYSLOG_ERR_SERVICE_STOP;
		break;

	case SERVICE_CONTROL_SHUTDOWN:
		syslog_status.SvcStatus.dwServiceSpecificExitCode = SYSLOG_ERR_SYSTEM_SHUTDOWN;
		break;

//	case SERVICE_CONTROL_INTERROGATE:
	default:
		return;
	}

	syslog_status.SvcStatus.dwCurrentState = SERVICE_STOP_PENDING;
	syslog_status.SvcStatus.dwWin32ExitCode = NO_ERROR;
	SetServiceStatus( syslog_status.SvcStatusHandle, &syslog_status.SvcStatus );
	syslog_status.shutdown = true;
	WSASetEvent( syslog_status.shutdown_event );
}

VOID WINAPI syslog_main( DWORD argc, LPTSTR argv[ ] )
{
	const byte msg_emerg_file_open[ ] = "<40>%SYSLOG: error opening log file";
	const byte msg_emerg_sock_init[ ] = "<40>%SYSLOG: error initializing WinSock: WSAStartup( ) failed";
	const byte msg_emerg_sock_evnt[ ] = "<40>%SYSLOG: error creating event: WSACreateEvent( ) failed";
	const byte msg_emerg_sock_open[ ] = "<40>%SYSLOG: error creating socket: socket( ) failed";
	const byte msg_emerg_sock_bind[ ] = "<40>%SYSLOG: error binding socket: bind( ) failed";
	const byte msg_emerg_sock_slct[ ] = "<40>%SYSLOG: error associating event: WSAEventSelect( ) failed";

	const byte msg_err_sock_read[ ] = "<43>%SYSLOG: error reading from the socket: recvfrom( ) failed";

	const byte msg_warn_msg_truncated[ ] = "<44>%SYSLOG: next message is truncated";

	const byte msg_note_syslog_started[ ] = "<45>%SYSLOG: running";
	const byte msg_note_syslog_stopping[ ] = "<45>%SYSLOG: stopping...";

	const byte msg_info_syslog_starting[ ] = "<46>%SYSLOG: " WINDOWSSYSLOG_VERSION_STRING " is starting...";
	const byte msg_info_file_new[ ] = "<46>%SYSLOG: new log file created";
	const byte msg_info_file_close[ ] = "<46>%SYSLOG: closing this log file";
	const byte msg_info_svc_stop[ ] = "<46>%SYSLOG: got SERVICE_CONTROL_STOP signal";
	const byte msg_info_sys_down[ ] = "<46>%SYSLOG: the system is shutting down";

	if( syslog_params.running_as_a_service )
	{
		syslog_status.SvcStatusHandle = RegisterServiceCtrlHandler( _T( "" ), SvcCtrlHandler );

		if( !syslog_status.SvcStatusHandle )
		{
			return;
		}
	}

	syslog_status.SvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
	syslog_status.SvcStatus.dwCurrentState = SERVICE_RUNNING;
	syslog_status.SvcStatus.dwControlsAccepted = 0;
	syslog_status.SvcStatus.dwWin32ExitCode = NO_ERROR;
	syslog_status.SvcStatus.dwServiceSpecificExitCode = SYSLOG_ERR_NOERROR;
	syslog_status.SvcStatus.dwCheckPoint = 0;
	syslog_status.SvcStatus.dwWaitHint = 0;

	if( syslog_params.running_as_a_service )
		SetServiceStatus( syslog_status.SvcStatusHandle, &syslog_status.SvcStatus );

	date_time initial_date = get_system_time( );

	syslog_file_t syslog_file( &syslog_params );

	if( !( syslog_file.compose_name( initial_date ) && syslog_file.open( ) ) )
	{
		syslog_status.SvcStatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
		syslog_status.SvcStatus.dwServiceSpecificExitCode = SYSLOG_ERR_FILEOPEN;

		goto EXIT_0;
	}

	syslog_file.log_own_msg( get_system_time( ), msg_info_syslog_starting );

	int Ret = 0;

	WSADATA wsaData;

	Ret = WSAStartup( MAKEWORD( 2, 2 ), &wsaData );

	if( Ret != 0 )
	{
		syslog_file.log_own_msg( get_system_time( ), msg_emerg_sock_init );

		syslog_status.SvcStatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
		syslog_status.SvcStatus.dwServiceSpecificExitCode = SYSLOG_ERR_WINSOCK_INIT;

		goto EXIT_1;
	}

	syslog_status.shutdown_event = WSACreateEvent( );
	WSAEVENT recv_from_event = WSACreateEvent( );
	WSAEVENT events[ 2 ] = { syslog_status.shutdown_event, recv_from_event };

	if(
		events[ 0 ] == WSA_INVALID_EVENT ||
		events[ 1 ] == WSA_INVALID_EVENT
	)
	{
		syslog_file.log_own_msg( get_system_time( ), msg_emerg_sock_evnt );

		syslog_status.SvcStatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
		syslog_status.SvcStatus.dwServiceSpecificExitCode = SYSLOG_ERR_WINSOCK_EVENT;

		goto EXIT_2;
	}

	SOCKET syslog_socket = udp_socket( );

	if( syslog_socket == INVALID_SOCKET )
	{
		syslog_file.log_own_msg( get_system_time( ), msg_emerg_sock_open );

		syslog_status.SvcStatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
		syslog_status.SvcStatus.dwServiceSpecificExitCode = SYSLOG_ERR_WINSOCK_SOCKET;

		goto EXIT_3;
	}

	SOCKADDR_IN syslog_addr;
	syslog_addr.sin_family = AF_INET;
	syslog_addr.sin_port = syslog_params.port;
	syslog_addr.sin_addr = syslog_params.address;

	Ret = bind( syslog_socket, ( SOCKADDR* ) &syslog_addr, sizeof( syslog_addr ) );

	if( Ret == SOCKET_ERROR )
	{
		syslog_file.log_own_msg( get_system_time( ), msg_emerg_sock_bind );

		syslog_status.SvcStatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
		syslog_status.SvcStatus.dwServiceSpecificExitCode = SYSLOG_ERR_WINSOCK_BIND;

		goto EXIT_4;
	}

	Ret = WSAEventSelect(
		syslog_socket,
		recv_from_event,
		FD_READ
	);

	if( Ret != 0 )
	{
		syslog_file.log_own_msg( get_system_time( ), msg_emerg_sock_slct );

		syslog_status.SvcStatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
		syslog_status.SvcStatus.dwServiceSpecificExitCode = SYSLOG_ERR_WINSOCK_SELECT;

		goto EXIT_4;
	}

	syslog_status.SvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;

	if( syslog_params.running_as_a_service )
		SetServiceStatus( syslog_status.SvcStatusHandle, &syslog_status.SvcStatus );

	syslog_file.log_own_msg( get_system_time( ), msg_note_syslog_started );

	while( true )
	{
		DWORD wait = WSAWaitForMultipleEvents(
			2,
			events,
			FALSE,
			WSA_INFINITE,
			FALSE
		);

		if( syslog_status.shutdown )
			break;

		WSAResetEvent( recv_from_event );

		const size_t BufLength = 4096;
		const size_t BufExtra = 4;
		byte ReceiveBuf[ BufLength + BufExtra ];

		SOCKADDR_IN SysLog_SenderAddr;
		int SysLog_SenderAddrSize = sizeof( SysLog_SenderAddr );

		Ret = recvfrom( syslog_socket, ( char* ) ReceiveBuf, BufLength, 0, ( SOCKADDR* ) &SysLog_SenderAddr, &SysLog_SenderAddrSize );

		date_time timestamp = get_system_time( );

		if( Ret == SOCKET_ERROR )
		{
			if( syslog_status.shutdown )
				break;
			else
			{
				if( ( Ret = WSAGetLastError( ) ) == WSAEMSGSIZE )
					syslog_file.log_own_msg( timestamp, msg_warn_msg_truncated );
				else
				{
					syslog_file.log_own_msg( timestamp, msg_err_sock_read );

					continue;
				}
			}
		}

		// sanitize the message:
		while(
			0 < Ret &&
			(
				ReceiveBuf[ Ret - 1 ] <= ' ' ||
				ReceiveBuf[ Ret - 1 ] > '\x7f'
			)
		)
			ReceiveBuf[ --Ret ] = '\0';

		for( int i = 0; i < Ret; ++i )
			if(
				ReceiveBuf[ i ] < ' ' ||
				ReceiveBuf[ i ] > '\x7f'
			)
				ReceiveBuf[ i ] = syslog_params.replacement;

		if( Ret > 0 ) // ignore empty and malformed packets ( Ret == 0 after sanitizing the buffer )
		{
			int extra_bytes = 0;

			if( syslog_params.newline_with_CR )
				ReceiveBuf[ Ret + extra_bytes++ ] = '\r';

			ReceiveBuf[ Ret + extra_bytes++ ] = '\n';

			ReceiveBuf[ Ret + extra_bytes + 1 ] = '\0'; // for printing the message as a c-string using printf, if needed

			if( !are_dates_the_same( initial_date, timestamp, syslog_params.logrotate_condition ) ) // log rotate condition
			{
				syslog_file_t new_syslog_file( &syslog_params );

				if( !( new_syslog_file.compose_name( timestamp ) && new_syslog_file.open( ) ) )
					syslog_file.log_own_msg( timestamp, msg_emerg_file_open );
				else
				{
					syslog_file.log_own_msg( timestamp, msg_info_file_close );

					syslog_file.takeup( new_syslog_file ); // also closes current logfile

					initial_date = timestamp;

					syslog_file.log_own_msg( timestamp, msg_info_file_new );
				}
			}

			syslog_file.log_message( timestamp, SysLog_SenderAddr.sin_addr, ReceiveBuf, Ret + extra_bytes );
		}
	} // while( true )

	if( syslog_params.running_as_a_service )
		switch( syslog_status.SvcStatus.dwServiceSpecificExitCode )
		{
		case SYSLOG_ERR_SERVICE_STOP:
			syslog_file.log_own_msg( get_system_time( ), msg_info_svc_stop );
			break;
		case SYSLOG_ERR_SYSTEM_SHUTDOWN:
			syslog_file.log_own_msg( get_system_time( ), msg_info_sys_down );
			break;
		}

EXIT_4:
	closesocket( syslog_socket );
	syslog_socket = INVALID_SOCKET;

EXIT_3:
	if( events[ 1 ] != WSA_INVALID_EVENT )
		WSACloseEvent( events[ 1 ] );
	if( events[ 0 ] != WSA_INVALID_EVENT )
		WSACloseEvent( events[ 0 ] );

EXIT_2:
	WSACleanup( );

EXIT_1:
	syslog_file.log_own_msg( get_system_time( ), msg_note_syslog_stopping );
	if( !syslog_params.using_stdout )
		syslog_file.log_own_msg( get_system_time( ), msg_info_file_close );
	syslog_file.close( );

EXIT_0:
	syslog_status.SvcStatus.dwCurrentState = SERVICE_STOPPED;
	syslog_status.SvcStatus.dwControlsAccepted = 0;

	if( syslog_params.running_as_a_service )
		SetServiceStatus( syslog_status.SvcStatusHandle, &syslog_status.SvcStatus );

	return;
}


BOOL ConsoleCtrlHandler( DWORD fdwCtrlType )
{
	switch( fdwCtrlType )
	{
	case CTRL_C_EVENT:
	case CTRL_CLOSE_EVENT:
	case CTRL_LOGOFF_EVENT:
	case CTRL_SHUTDOWN_EVENT:
		syslog_status.SvcStatus.dwCurrentState = SERVICE_STOP_PENDING;
		syslog_status.SvcStatus.dwWin32ExitCode = NO_ERROR;
		syslog_status.SvcStatus.dwServiceSpecificExitCode = SYSLOG_ERR_NOERROR;
		syslog_status.shutdown = true;
		WSASetEvent( syslog_status.shutdown_event );
		return TRUE;

	case CTRL_BREAK_EVENT:
		Beep( 900, 200 );
		if( !syslog_params.no_dump )
			printf( " * * * I am logging! Press 'Ctrl + C' to stop.\n" );
		return TRUE;

	default:
		return FALSE;
	}
}


int _tmain( int argc, TCHAR** argv )
{
	syslog_status.init( );

	if( ! syslog_params.init( argc - 1, argv + 1 ) )
	{
		if( ! syslog_params.running_as_a_service )	// we have console
			_ftprintf( stderr, _T( "Error initializing parameters - check for invalid parameters, especially too long paths/names. Exiting now.\n" ) );

		return 1;
	}

	if( syslog_params.running_as_a_service )
	{
		SERVICE_TABLE_ENTRY dispatch_table[ ] =
		{
			{ _T( "" ), syslog_main },
			{ nullptr, nullptr }
		};

		StartServiceCtrlDispatcher( dispatch_table ); // does not return until all running services in the process have entered the SERVICE_STOPPED state

		return 0;
	}

	SetConsoleCtrlHandler( ( PHANDLER_ROUTINE ) ConsoleCtrlHandler, TRUE );

	if( ! syslog_params.no_dump )
	{
		_ftprintf( stdout, _T( WINDOWSSYSLOG_VERSION_STRING ) _T( "\n" ) );
		_ftprintf( stdout, _T( "Please visit http://windowssyslog.codeplex.com/ for the documentation and new releases.\n" ) );

		syslog_params.dump( );

		if( ! syslog_params.no_run )
			_ftprintf( stdout, _T( " ** Now running syslog. Press 'Ctrl + C' to stop.\n" ) );
	}

	if( ! syslog_params.no_run )
		syslog_main( 0, nullptr );

	if( ! ( syslog_params.no_dump || syslog_params.no_run ) )
		_ftprintf(
			stdout,
			_T( " ** Syslog stopped. Return code is {%d}, which means {%s}.\n" ),
			syslog_status.SvcStatus.dwServiceSpecificExitCode,
			SyslogErrorMessages[ syslog_status.SvcStatus.dwServiceSpecificExitCode ]
		);

	return syslog_status.SvcStatus.dwServiceSpecificExitCode;
}
